/*
 * Copyright (C) 2000 - 2024 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception.  You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package org.silverpeas.core.workflow.engine.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.silverpeas.core.contribution.content.form.DataRecord;
import org.silverpeas.core.contribution.content.form.FieldTemplate;
import org.silverpeas.core.contribution.content.form.FormException;
import org.silverpeas.core.contribution.content.form.RecordSet;
import org.silverpeas.core.contribution.content.form.RecordTemplate;
import org.silverpeas.core.contribution.content.form.dummy.DummyRecordSet;
import org.silverpeas.core.contribution.content.form.form.HtmlForm;
import org.silverpeas.core.contribution.content.form.form.XmlForm;
import org.silverpeas.core.contribution.content.form.record.GenericFieldTemplate;
import org.silverpeas.core.contribution.content.form.record.GenericRecordSet;
import org.silverpeas.core.contribution.content.form.record.GenericRecordSetManager;
import org.silverpeas.core.contribution.content.form.record.GenericRecordTemplate;
import org.silverpeas.core.contribution.content.form.record.IdentifiedRecordTemplate;
import org.silverpeas.kernel.logging.SilverLogger;
import org.silverpeas.core.workflow.api.model.Action;
import org.silverpeas.core.workflow.api.model.ContextualDesignation;
import org.silverpeas.core.workflow.api.model.Form;
import org.silverpeas.core.workflow.api.model.QualifiedUsers;
import org.silverpeas.core.workflow.api.model.State;
import org.silverpeas.core.workflow.api.model.States;
import org.silverpeas.core.workflow.engine.datarecord.ProcessInstanceRecordTemplate;
import org.silverpeas.core.workflow.engine.datarecord.ProcessInstanceRowTemplate;
import org.silverpeas.kernel.util.StringUtil;
import org.silverpeas.core.workflow.api.WorkflowException;
import org.silverpeas.core.workflow.api.model.Actions;
import org.silverpeas.core.workflow.api.model.ContextualDesignations;
import org.silverpeas.core.workflow.api.model.DataFolder;
import org.silverpeas.core.workflow.api.model.Forms;
import org.silverpeas.core.workflow.api.model.Participant;
import org.silverpeas.core.workflow.api.model.Participants;
import org.silverpeas.core.workflow.api.model.Presentation;
import org.silverpeas.core.workflow.api.model.ProcessModel;
import org.silverpeas.core.workflow.api.model.RelatedUser;
import org.silverpeas.core.workflow.api.model.Role;
import org.silverpeas.core.workflow.api.model.Roles;
import org.silverpeas.core.workflow.api.model.UserInRole;
import org.silverpeas.core.workflow.engine.WorkflowHub;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * Class implementing the representation of the main &lt;processModel&gt; element of a Process
 * Model.
 */
@XmlRootElement(name = "processModel")
@XmlAccessorType(XmlAccessType.NONE)
public class ProcessModelImpl implements ProcessModel, Serializable {
  private static final long serialVersionUID = -4576686557632464607L;
  private static final String PROCESS_MODEL = "ProcessModel";
  private String modelId;
  @XmlAttribute
  private String name;
  @XmlElement(name = "label", type = SpecificLabel.class)
  private List<ContextualDesignation> labels;
  @XmlElement(name = "description", type = SpecificLabel.class)
  private List<ContextualDesignation> descriptions;
  @XmlElement(type = RolesImpl.class)
  private Roles roles;
  @XmlElement(type = PresentationImpl.class)
  private Presentation presentation;
  @XmlElement(type = ParticipantsImpl.class)
  private Participants participants;
  @XmlElement(type = StatesImpl.class)
  private States states;
  @XmlElement(type = ActionsImpl.class)
  private Actions actions;
  @XmlElement(type = DataFolderImpl.class)
  private DataFolder dataFolder;
  @XmlElement(type = DataFolderImpl.class)
  private DataFolder userInfos;
  @XmlElement(type = FormsImpl.class)
  private Forms forms;

  private final HashMap<String, RecordTemplate> instanceDataTemplates = new HashMap<>();
  private final HashMap<String, RecordTemplate> rowTemplates = new HashMap<>();

  /**
   * Constructor
   */
  public ProcessModelImpl() {
    labels = new ArrayList<>();
    descriptions = new ArrayList<>();
    presentation = createPresentation();
    dataFolder = new DataFolderImpl();
  }

  @Override
  public String getModelId() {
    return this.modelId;
  }

  @Override
  public void setModelId(String modelId) {
    this.modelId = modelId;
  }

  @Override
  public String getName() {
    return this.name;
  }

  @Override
  public void setName(String name) {
    this.name = name;
  }

  @Override
  public Presentation getPresentation() {
    return presentation;
  }

  @Override
  public void setPresentation(Presentation presentation) {
    this.presentation = presentation;
  }

  @Override
  public Presentation createPresentation() {
    return new PresentationImpl();
  }

  @Override
  public Participant[] getParticipants() {
    if (participants == null) {
      return new Participant[0];
    }
    return participants.getParticipants();
  }

  @Override
  public Participants getParticipantsEx() {
    return participants;
  }

  @Override
  public void setParticipants(Participants participants) {
    this.participants = participants;
  }

  @Override
  public Participants createParticipants() {
    return new ParticipantsImpl();
  }

  @Override
  public Role[] getRoles() {
    if (roles == null) {
      return new Role[0];
    }
    return roles.getRoles();
  }

  @Override
  public Role getRole(String name) {
    if (roles == null) {
      return null;
    }
    return roles.getRole(name);
  }

  @Override
  public Roles getRolesEx() {
    return roles;
  }

  @Override
  public void setRoles(Roles roles) {
    this.roles = roles;
  }

  @Override
  public Roles createRoles() {
    return new RolesImpl();
  }

  @Override
  public State[] getStates() {
    if (states == null) {
      return new State[0];
    }
    return states.getStates();
  }

  @Override
  public States getStatesEx() {
    return states;
  }

  @Override
  public State getState(String name) {
    if (states == null) {
      return null;
    }
    return states.getState(name);
  }

  @Override
  public void setStates(States states) {
    this.states = states;
  }

  @Override
  public States createStates() {
    return new StatesImpl();
  }

  @Override
  public Action[] getActions() {
    if (actions == null) {
      return new Action[0];
    }
    return actions.getActions();
  }

  @Override
  public Actions getActionsEx() {
    return actions;
  }

  @Override
  public Action getAction(String name) throws WorkflowException {
    if (actions == null) {
      return null;
    }
    return actions.getAction(name);
  }

  @Override
  public void setActions(Actions actions) {
    this.actions = actions;
  }

  @Override
  public Actions createActions() {
    return new ActionsImpl();
  }

  @Override
  public DataFolder getDataFolder() {
    return dataFolder;
  }

  @Override
  public void setDataFolder(DataFolder dataFolder) {
    this.dataFolder = dataFolder;
  }

  @Override
  public DataFolder createDataFolder() {
    return new DataFolderImpl();
  }

  @Override
  public DataFolder getUserInfos() {
    return userInfos;
  }

  @Override
  public void setUserInfos(DataFolder userInfos) {
    this.userInfos = userInfos;
  }

  @Override
  public Forms getForms() {
    return forms;
  }

  @Override
  public Form getForm(String name) {
    return getForm(name, null);
  }

  @Override
  public Form getForm(String name, String role) {
    if (forms == null) {
      return null;
    }
    return forms.getForm(name, role);
  }

  @Override
  public void setForms(Forms forms) {
    this.forms = forms;
  }

  @Override
  public Forms createForms() {
    return new FormsImpl();
  }

  @Override
  public ContextualDesignations getLabels() {
    return new SpecificLabelListHelper(labels);
  }

  @Override
  public String getLabel(String role, String language) {
    return getLabels().getLabel(role, language);
  }

  @Override
  public ContextualDesignations getDescriptions() {
    return new SpecificLabelListHelper(descriptions);
  }

  @Override
  public String getDescription(String role, String language) {
    return getDescriptions().getLabel(role, language);
  }

  @Override
  public QualifiedUsers createQualifiedUsers() {
    return new QualifiedUsersImpl();
  }

  @Override
  public RelatedUser createRelatedUser() {
    return new RelatedUserImpl();
  }

  @Override
  public String getFolderRecordSetName() {
    return modelId + ":" + "folder";
  }

  @Override
  public String getFormRecordSetName(String formName) {
    return modelId + ":" + "form:" + formName;
  }

  @Override
  public RecordSet getFolderRecordSet() throws WorkflowException {
    // Now, the folder is read from xml file and no more in database
    // This permit to add items to the folder
    IdentifiedRecordTemplate idTemplate;
    int templateId;
    try {
      RecordSet recordSet = getGenericRecordSetManager().getRecordSet(getFolderRecordSetName());
      idTemplate = (IdentifiedRecordTemplate) recordSet.getRecordTemplate();
      templateId = idTemplate.getInternalId();
    } catch (FormException e) {
      throw new WorkflowException(PROCESS_MODEL, "workflowEngine.EXP_UNKNOWN_RECORD_SET",
          getFolderRecordSetName(), e);
    }

    RecordTemplate template = dataFolder.toRecordTemplate(null, null, false);
    idTemplate = new IdentifiedRecordTemplate(template);
    idTemplate.setExternalId(getFolderRecordSetName());
    idTemplate.setInternalId(templateId);
    return new GenericRecordSet(idTemplate);
  }

  @Override
  public RecordSet getFormRecordSet(String formName) throws WorkflowException {
    try {
      RecordSet recordSet =
          getGenericRecordSetManager().getRecordSet(getFormRecordSetName(formName));

      /*
       * If recordset cannot be found, form is a new Form declared after peas instantiation: add it
       */
      if (recordSet instanceof DummyRecordSet) {
        Form form = getForm(formName);
        RecordTemplate template = form.toRecordTemplate(null, null);
        getGenericRecordSetManager().createRecordSet(getFormRecordSetName(formName), template);
        recordSet = getGenericRecordSetManager().getRecordSet(getFormRecordSetName(formName));
      }

      IdentifiedRecordTemplate template = (IdentifiedRecordTemplate) recordSet.getRecordTemplate();

      GenericRecordTemplate wrapped = (GenericRecordTemplate) template.getWrappedTemplate();

      GenericFieldTemplate fieldTemplate;
      String fieldName;
      String fieldType;
      boolean isMandatory;
      boolean isReadOnly;
      boolean isHidden;
      FormImpl form = (FormImpl) getForm(formName);
      FieldTemplate[] fields = form.toRecordTemplate(null, null).getFieldTemplates();

      for (final FieldTemplate field : fields) {
        fieldName = field.getFieldName();
        fieldType = field.getTypeName();
        isMandatory = field.isMandatory();
        isReadOnly = field.isReadOnly();
        isHidden = field.isHidden();

        fieldTemplate = new GenericFieldTemplate(fieldName, fieldType);
        fieldTemplate.setMandatory(isMandatory);
        fieldTemplate.setReadOnly(isReadOnly);
        fieldTemplate.setHidden(isHidden);

        wrapped.addFieldTemplate(fieldTemplate);
      }

      return recordSet;
    } catch (FormException e) {
      throw new WorkflowException(PROCESS_MODEL, "workflowEngine.EXP_UNKNOWN_RECORD_SET",
          getFormRecordSetName(formName), e);
    }
  }

  @Override
  public Form getActionForm(String actionName) throws WorkflowException {
    Action action = getAction(actionName);
    return action.getForm();
  }

  @Override
  public Action getCreateAction(String role) throws WorkflowException {
    Action[] someActions = getActions();

    for (Action action : someActions) {
      if ("create".equals(action.getKind())) {
        // Retrieve roles allowed to do this action
        QualifiedUsers creators = action.getAllowedUsers();
        UserInRole[] usersInRoles = creators.getUserInRoles();

        for (UserInRole usersInRole : usersInRoles) {
          if (role.equals(usersInRole.getRoleName())) {
            return action;
          }
        }
      }
    }

    throw new WorkflowException(PROCESS_MODEL, "workflowEngine.ERR_NO_CREATE_ACTION_DEFINED");
  }

  @Override
  public org.silverpeas.core.contribution.content.form.Form getPublicationForm(String actionName, String roleName,
      String lang) throws WorkflowException {
    Action action = getAction(actionName);
    if (action == null || action.getForm() == null) {
      return null;
    }

    return getConcreteForm(action.getForm(), roleName, lang, false);
  }

  @Override
  public org.silverpeas.core.contribution.content.form.Form getPresentationForm(String name, String roleName, String lang)
      throws WorkflowException {
    Action action = null;
    Form form;

    if (!"presentationForm".equalsIgnoreCase(name)) {
      try {
        action = getAction(name);
      } catch (WorkflowException e) {
        SilverLogger.getLogger(this).warn(e.getMessage());
      }
    }

    if (action != null) {
      form = action.getForm();
    } else {
      form = getForm(name, roleName);
    }

    if (form == null) {
      return null;
    }

    return getConcreteForm(form, roleName, lang, true);
  }

  private org.silverpeas.core.contribution.content.form.Form getConcreteForm(Form form,
      String roleName, String lang, boolean readOnly) throws WorkflowException {
    try {
      if (StringUtil.isDefined(form.getHTMLFileName())) {
        HtmlForm htmlForm = new HtmlForm(form.toRecordTemplate(roleName, lang));
        htmlForm.setFileName(WorkflowHub.getProcessModelManager().getProcessModelDir() +
            form.getHTMLFileName());
        htmlForm.setName(form.getName());
        htmlForm.setTitle(form.getTitle(roleName, lang));
        return htmlForm;
      } else {
        XmlForm xmlForm = new XmlForm(form.toRecordTemplate(roleName, lang, readOnly));
        xmlForm.setName(form.getName());
        xmlForm.setTitle(form.getTitle(roleName, lang));
        return xmlForm;
      }
    } catch (FormException e) {
      throw new WorkflowException(PROCESS_MODEL, "workflowEngine.EXP_ILL_FORMED_FORM",
          form.getName(), e);
    }
  }

  /**
   * Returns an empty DataRecord which must be filled in order to process the
   * named action. Returns null if no form is required to process this action. Throws a
   * WorkflowException if the action is unknown.
   * @param actionName Name of the action
   * @param roleName  Role of the current user
   * @param lang Lang of the current user
   */
  @Override
  public DataRecord getNewActionRecord(String actionName, String roleName, String lang,
      DataRecord data) throws WorkflowException {
    Action action = getAction(actionName);
    if (action == null || action.getForm() == null) {
      return null;
    }
    return action.getForm().getDefaultRecord(roleName, lang, data);
  }

  /**
   * Returns an empty DataRecord which must be filled in order to fill the user
   * information Throws a WorkflowException if problem encountered.
   * @param roleName Role of the current user
   * @param lang Lang of the current user
   */
  @Override
  public DataRecord getNewUserInfosRecord(String roleName, String lang) throws WorkflowException {
    try {
      return this.getUserInfos().toRecordTemplate(roleName, lang, false).getEmptyRecord();
    } catch (FormException e) {
      throw new WorkflowException(PROCESS_MODEL, "workflowEngine.EXP_ILL_FORMED_FORM",
          "User Infos", e);
    }
  }

  /**
   * Returns the roles under which an user can create a new instance
   */
  @Override
  public String[] getCreationRoles() throws WorkflowException {
    try {
      List<String> someRoles = new ArrayList<>();

      // Search for actions of kind create
      Action[] someActions = getActions();
      for (Action action : someActions) {
        if ("create".equals(action.getKind())) {
          // Retrieve roles allowed to do this action
          QualifiedUsers creators = action.getAllowedUsers();
          UserInRole[] usersInRoles = creators.getUserInRoles();

          for (UserInRole usersInRole : usersInRoles) {
            if (!someRoles.contains(usersInRole.getRoleName())) {
              someRoles.add(usersInRole.getRoleName());
            }
          }
        }
      }

      return someRoles.toArray(new String[0]);
    } catch (Exception e) {
      throw new WorkflowException(PROCESS_MODEL, "workflowEngine.EXP_FAIL_GET_CREATION_ROLES",
          this.name, e);
    }
  }

  /**
   * Returns the recordTemplate which describes the data record of the process instance built from
   * this model.
   * @param role Role of the current user
   * @param lang Lang of the current user
   */
  @Override
  public RecordTemplate getAllDataTemplate(String role, String lang) {
    RecordTemplate template = instanceDataTemplates.get(role + "\n" + lang);

    if (template == null) {
      template =
          new ProcessInstanceRecordTemplate(this, role,
              lang);
      instanceDataTemplates.put(role + "\n" + lang, template);
    }
    return template;
  }

  /**
   * Returns the recordTemplate which describes the data record used to show process instance as a
   * row in list.
   * @param role Role of the current user
   * @param lang Lang of the current user
   */
  @Override
  public RecordTemplate getRowTemplate(String role, String lang) {
    RecordTemplate template = rowTemplates.get(role + "\n" + lang);

    if (template == null) {
      template =
          new ProcessInstanceRowTemplate(this, role,
              lang);
      rowTemplates.put(role + "\n" + lang, template);
    }
    return template;
  }

  /**
   * Returns the recordTemplate which describes the data record used to show process instance as a
   * row in list.
   * @param role Role of the current user
   * @param lang Lang of the current user
   * @param isProcessIdVisible Case if id is deplayed or not
   */
  @Override
  public RecordTemplate getRowTemplate(String role, String lang, boolean isProcessIdVisible) {
    RecordTemplate template = rowTemplates.get(role + "\n" + lang);
    if (template == null) {
      template = initRowTemplate(role, lang, isProcessIdVisible);
    }
    else {
      FieldTemplate instanceIdField = null;
      try {
        instanceIdField = template.getFieldTemplate("instance.id");
      } catch (FormException e) {
        //Do nothing
      }
      boolean isProcessIdVisibleField = instanceIdField!=null;
      if ((!isProcessIdVisibleField && isProcessIdVisible) || (isProcessIdVisibleField && !isProcessIdVisible)) {
        template = initRowTemplate(role, lang, isProcessIdVisible);
      }
    }
    return template;
  }

  /**
   * Init a new row Template
   * @param role Role of the current user
   * @param lang Lang of the current user
   * @param isProcessIdVisible Case if id is deplayed or not
   * */
  private RecordTemplate initRowTemplate(String role, String lang, boolean isProcessIdVisible) {
    RecordTemplate template = new ProcessInstanceRowTemplate(this, role,
            lang, isProcessIdVisible);
    rowTemplates.put(role + "\n" + lang, template);
    return template;
  }

  /**
   * Gets an instance of a GenericRecordSet objects manager.
   * @return a GenericRecordSetManager instance.
   */
  private GenericRecordSetManager getGenericRecordSetManager() {
    return GenericRecordSetManager.getInstance();
  }
}
